// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:kernel/ast.dart';
import 'package:kernel/core_types.dart';

import '../source/source_library_builder.dart';

const String lateFieldPrefix = '_#';
const String lateIsSetSuffix = '#isSet';
const String lateLocalPrefix = '#';
const String lateLocalGetterSuffix = '#get';
const String lateLocalSetterSuffix = '#set';

/// Creates the body for the synthesized getter used to encode the lowering
/// of a late non-final field or local with an initializer.
///
/// Late final fields and locals need to detect writes during initialization and
/// therefore uses [createGetterWithInitializerWithRecheck] instead.
Statement createGetterWithInitializer(
  CoreTypes coreTypes,
  int fileOffset,
  String name,
  DartType type,
  Expression initializer, {
  required Expression createVariableRead({bool needsPromotion}),
  required Expression createVariableWrite(Expression value),
  required Expression createIsSetRead(),
  required Expression createIsSetWrite(Expression value),
  required IsSetEncoding isSetEncoding,
}) {
  switch (isSetEncoding) {
    case IsSetEncoding.useIsSetField:
      // Generate:
      //
      //    if (!_#isSet#field) {
      //      _#field = <init>;
      //      _#isSet#field = true
      //    }
      //    return _#field;
      return new Block(<Statement>[
        new IfStatement(
          new Not(createIsSetRead()..fileOffset = fileOffset)
            ..fileOffset = fileOffset,
          new Block(<Statement>[
            new ExpressionStatement(
              createVariableWrite(initializer)..fileOffset = fileOffset,
            )..fileOffset = fileOffset,
            new ExpressionStatement(
              createIsSetWrite(new BoolLiteral(true)..fileOffset = fileOffset)
                ..fileOffset = fileOffset,
            )..fileOffset = fileOffset,
          ]),
          null,
        )..fileOffset = fileOffset,
        new ReturnStatement(
          // If [type] is a type parameter with undetermined nullability we
          // need to create a read of the field that is promoted to the type
          // variable type.
          createVariableRead(needsPromotion: type.isPotentiallyNonNullable),
        )..fileOffset = fileOffset,
      ])..fileOffset = fileOffset;
    case IsSetEncoding.useSentinel:
      // Generate:
      //
      //    return let # = _#field in isSentinel(#) ? _#field = <init> : #;
      VariableDeclaration variable = new VariableDeclaration.forValue(
        createVariableRead(needsPromotion: false)..fileOffset = fileOffset,
        type: type.withDeclaredNullability(Nullability.nullable),
      )..fileOffset = fileOffset;
      return new ReturnStatement(
        new Let(
          variable,
          new ConditionalExpression(
            new StaticInvocation(
              coreTypes.isSentinelMethod,
              new Arguments(<Expression>[
                new VariableGet(variable)..fileOffset = fileOffset,
              ])..fileOffset = fileOffset,
            )..fileOffset = fileOffset,
            createVariableWrite(initializer)..fileOffset = fileOffset,
            new VariableGet(variable, type)..fileOffset = fileOffset,
            type,
          )..fileOffset = fileOffset,
        )..fileOffset = fileOffset,
      )..fileOffset = fileOffset;
    case IsSetEncoding.useNull:
      // Generate:
      //
      //    return let # = _#field in # == null ? _#field = <init> : #;
      VariableDeclaration variable = new VariableDeclaration.forValue(
        createVariableRead(needsPromotion: false)..fileOffset = fileOffset,
        type: type.withDeclaredNullability(Nullability.nullable),
      )..fileOffset = fileOffset;
      return new ReturnStatement(
        new Let(
          variable,
          new ConditionalExpression(
            new EqualsNull(new VariableGet(variable)..fileOffset = fileOffset)
              ..fileOffset = fileOffset,
            createVariableWrite(initializer)..fileOffset = fileOffset,
            new VariableGet(variable, type)..fileOffset = fileOffset,
            type,
          )..fileOffset = fileOffset,
        )..fileOffset = fileOffset,
      )..fileOffset = fileOffset;
  }
}

/// Creates the body for the synthesized getter used to encode the lowering
/// of a late final field or local with an initializer.
Statement createGetterWithInitializerWithRecheck(
  CoreTypes coreTypes,
  int fileOffset,
  String name,
  DartType type,
  Expression initializer, {
  required Expression createVariableRead({bool needsPromotion}),
  required Expression createVariableWrite(Expression value),
  required Expression createIsSetRead(),
  required Expression createIsSetWrite(Expression value),
  required IsSetEncoding isSetEncoding,
  required bool forField,
}) {
  Constructor constructor = forField
      ? coreTypes.lateInitializationFieldAssignedDuringInitializationConstructor
      : coreTypes
            .lateInitializationLocalAssignedDuringInitializationConstructor;
  Expression exception =
      new Throw(
          new ConstructorInvocation(
            constructor,
            new Arguments(<Expression>[
              new StringLiteral(name)..fileOffset = fileOffset,
            ])..fileOffset = fileOffset,
          )..fileOffset = fileOffset,
        )
        ..fileOffset = fileOffset
        ..forErrorHandling = true;
  VariableDeclaration temp = new VariableDeclaration.forValue(
    initializer,
    type: type,
  )..fileOffset = fileOffset;
  switch (isSetEncoding) {
    case IsSetEncoding.useIsSetField:
      // Generate:
      //
      //    if (!_#isSet#field) {
      //      var temp = <init>;
      //      if (_#isSet#field) throw '...'
      //      _#field = temp;
      //      _#isSet#field = true
      //    }
      //    return _#field;
      return new Block(<Statement>[
        new IfStatement(
          new Not(createIsSetRead()..fileOffset = fileOffset)
            ..fileOffset = fileOffset,
          new Block(<Statement>[
            temp,
            new IfStatement(
              createIsSetRead()..fileOffset = fileOffset,
              new ExpressionStatement(exception)..fileOffset = fileOffset,
              null,
            )..fileOffset = fileOffset,
            new ExpressionStatement(
              createVariableWrite(
                new VariableGet(temp)..fileOffset = fileOffset,
              )..fileOffset = fileOffset,
            )..fileOffset = fileOffset,
            new ExpressionStatement(
              createIsSetWrite(new BoolLiteral(true)..fileOffset = fileOffset)
                ..fileOffset = fileOffset,
            )..fileOffset = fileOffset,
          ]),
          null,
        )..fileOffset = fileOffset,
        new ReturnStatement(
          // If [type] is a type parameter with undetermined nullability we
          // need to create a read of the field that is promoted to the type
          // variable type.
          createVariableRead(needsPromotion: type.isPotentiallyNonNullable),
        )..fileOffset = fileOffset,
      ])..fileOffset = fileOffset;
    case IsSetEncoding.useSentinel:
      // Generate:
      //
      //    return let #1 = _#field in isSentinel(#1)
      //        ? let #2 = <init> in isSentinel(_#field)
      //            ? _#field = #2 : throw '...'
      //        : #1;
      VariableDeclaration variable = new VariableDeclaration.forValue(
        createVariableRead(needsPromotion: false)..fileOffset = fileOffset,
        type: type,
      )..fileOffset = fileOffset;
      return new ReturnStatement(
        new Let(
          variable,
          new ConditionalExpression(
            new StaticInvocation(
              coreTypes.isSentinelMethod,
              new Arguments(<Expression>[
                new VariableGet(variable)..fileOffset = fileOffset,
              ])..fileOffset = fileOffset,
            )..fileOffset = fileOffset,
            new Let(
              temp,
              new ConditionalExpression(
                new StaticInvocation(
                  coreTypes.isSentinelMethod,
                  new Arguments(<Expression>[
                    createVariableRead(needsPromotion: false)
                      ..fileOffset = fileOffset,
                  ])..fileOffset = fileOffset,
                )..fileOffset = fileOffset,
                createVariableWrite(
                  new VariableGet(temp)..fileOffset = fileOffset,
                )..fileOffset = fileOffset,
                exception,
                type,
              )..fileOffset = fileOffset,
            ),
            new VariableGet(variable)..fileOffset = fileOffset,
            type,
          )..fileOffset = fileOffset,
        )..fileOffset = fileOffset,
      )..fileOffset = fileOffset;
    case IsSetEncoding.useNull:
      // Generate:
      //
      //    return let #1 = _#field in #1 == null
      //        ? let #2 = <init> in _#field == null
      //            ? _#field = #2 : throw '...'
      //        : #1;
      VariableDeclaration variable = new VariableDeclaration.forValue(
        createVariableRead(needsPromotion: false)..fileOffset = fileOffset,
        type: type.withDeclaredNullability(Nullability.nullable),
      )..fileOffset = fileOffset;
      return new ReturnStatement(
        new Let(
          variable,
          new ConditionalExpression(
            new EqualsNull(new VariableGet(variable)..fileOffset = fileOffset)
              ..fileOffset = fileOffset,
            new Let(
              temp,
              new ConditionalExpression(
                new EqualsNull(
                  createVariableRead(needsPromotion: false)
                    ..fileOffset = fileOffset,
                )..fileOffset = fileOffset,
                createVariableWrite(
                  new VariableGet(temp)..fileOffset = fileOffset,
                )..fileOffset = fileOffset,
                exception,
                type,
              )..fileOffset = fileOffset,
            ),
            new VariableGet(variable, type)..fileOffset = fileOffset,
            type,
          )..fileOffset = fileOffset,
        )..fileOffset = fileOffset,
      )..fileOffset = fileOffset;
  }
}

/// Creates the body for the synthesized getter used to encode the lowering
/// of a late field or local without an initializer.
Statement createGetterBodyWithoutInitializer(
  CoreTypes coreTypes,
  int fileOffset,
  String name,
  DartType type, {
  required Expression createVariableRead({bool needsPromotion}),
  required Expression createIsSetRead(),
  required IsSetEncoding isSetEncoding,
  required bool forField,
}) {
  Expression exception =
      new Throw(
          new ConstructorInvocation(
            forField
                ? coreTypes.lateInitializationFieldNotInitializedConstructor
                : coreTypes.lateInitializationLocalNotInitializedConstructor,
            new Arguments(<Expression>[
              new StringLiteral(name)..fileOffset = fileOffset,
            ])..fileOffset = fileOffset,
          )..fileOffset = fileOffset,
        )
        ..fileOffset = fileOffset
        ..forErrorHandling = true;
  switch (isSetEncoding) {
    case IsSetEncoding.useIsSetField:
      // Generate:
      //
      //    return _#isSet#field ? _#field : throw '...';
      return new ReturnStatement(
        new ConditionalExpression(
          createIsSetRead()..fileOffset = fileOffset,
          createVariableRead(needsPromotion: type.isPotentiallyNonNullable)
            ..fileOffset = fileOffset,
          exception,
          type,
        )..fileOffset = fileOffset,
      )..fileOffset = fileOffset;
    case IsSetEncoding.useSentinel:
      // Generate:
      //
      //    return let # = _#field in isSentinel(#) ? throw '...' : #;
      VariableDeclaration variable = new VariableDeclaration.forValue(
        createVariableRead()..fileOffset = fileOffset,
        type: type.withDeclaredNullability(Nullability.nullable),
      )..fileOffset = fileOffset;
      return new ReturnStatement(
        new Let(
          variable,
          new ConditionalExpression(
            new StaticInvocation(
              coreTypes.isSentinelMethod,
              new Arguments(<Expression>[
                new VariableGet(variable)..fileOffset = fileOffset,
              ])..fileOffset = fileOffset,
            )..fileOffset = fileOffset,
            exception,
            new VariableGet(variable, type)..fileOffset = fileOffset,
            type,
          )..fileOffset = fileOffset,
        )..fileOffset = fileOffset,
      )..fileOffset = fileOffset;
    case IsSetEncoding.useNull:
      // Generate:
      //
      //    return let # = _#field in # == null ? throw '...' : #;
      VariableDeclaration variable = new VariableDeclaration.forValue(
        createVariableRead()..fileOffset = fileOffset,
        type: type.withDeclaredNullability(Nullability.nullable),
      )..fileOffset = fileOffset;
      return new ReturnStatement(
        new Let(
          variable,
          new ConditionalExpression(
            new EqualsNull(new VariableGet(variable)..fileOffset = fileOffset)
              ..fileOffset = fileOffset,
            exception,
            new VariableGet(variable, type)..fileOffset = fileOffset,
            type,
          )..fileOffset = fileOffset,
        )..fileOffset = fileOffset,
      )..fileOffset = fileOffset;
  }
}

/// Creates the body for the synthesized setter used to encode the lowering
/// of a non-final late field or local.
Statement createSetterBody(
  CoreTypes coreTypes,
  int fileOffset,
  String name,
  VariableDeclaration parameter,
  DartType type, {
  required bool shouldReturnValue,
  required Expression createVariableWrite(Expression value),
  required Expression createIsSetWrite(Expression value),
  required IsSetEncoding isSetEncoding,
}) {
  Statement createReturn(Expression value) {
    if (shouldReturnValue) {
      return new ReturnStatement(value)..fileOffset = fileOffset;
    } else {
      return new ExpressionStatement(value)..fileOffset = fileOffset;
    }
  }

  Statement assignment = createReturn(
    createVariableWrite(new VariableGet(parameter)..fileOffset = fileOffset)
      ..fileOffset = fileOffset,
  );

  switch (isSetEncoding) {
    case IsSetEncoding.useIsSetField:
      // Generate:
      //
      //    _#isSet#field = true;
      //    return _#field = parameter
      //
      return new Block([
        new ExpressionStatement(
          createIsSetWrite(new BoolLiteral(true)..fileOffset = fileOffset)
            ..fileOffset = fileOffset,
        )..fileOffset = fileOffset,
        assignment,
      ])..fileOffset = fileOffset;
    case IsSetEncoding.useSentinel:
    case IsSetEncoding.useNull:
      // Generate:
      //
      //    return _#field = parameter
      //
      return assignment;
  }
}

/// Creates the body for the synthesized setter used to encode the lowering
/// of a final late field or local.
Statement createSetterBodyFinal(
  CoreTypes coreTypes,
  int fileOffset,
  String name,
  VariableDeclaration parameter,
  DartType type, {
  required bool shouldReturnValue,
  required Expression createVariableRead(),
  required Expression createVariableWrite(Expression value),
  required Expression createIsSetRead(),
  required Expression createIsSetWrite(Expression value),
  required IsSetEncoding isSetEncoding,
  required bool forField,
}) {
  Expression exception =
      new Throw(
          new ConstructorInvocation(
            forField
                ? coreTypes.lateInitializationFieldAlreadyInitializedConstructor
                : coreTypes
                      .lateInitializationLocalAlreadyInitializedConstructor,
            new Arguments(<Expression>[
              new StringLiteral(name)..fileOffset = fileOffset,
            ])..fileOffset = fileOffset,
          )..fileOffset = fileOffset,
        )
        ..fileOffset = fileOffset
        ..forErrorHandling = true;

  Statement createReturn(Expression value) {
    if (shouldReturnValue) {
      return new ReturnStatement(value)..fileOffset = fileOffset;
    } else {
      return new ExpressionStatement(value)..fileOffset = fileOffset;
    }
  }

  switch (isSetEncoding) {
    case IsSetEncoding.useIsSetField:
      // Generate:
      //
      //    if (_#isSet#field) {
      //      throw '...';
      //    } else
      //      _#isSet#field = true;
      //      return _#field = parameter
      //    }
      return new IfStatement(
        createIsSetRead()..fileOffset = fileOffset,
        new ExpressionStatement(exception)..fileOffset = fileOffset,
        new Block([
          new ExpressionStatement(
            createIsSetWrite(new BoolLiteral(true)..fileOffset = fileOffset)
              ..fileOffset = fileOffset,
          )..fileOffset = fileOffset,
          createReturn(
            createVariableWrite(
              new VariableGet(parameter)..fileOffset = fileOffset,
            )..fileOffset = fileOffset,
          ),
        ])..fileOffset = fileOffset,
      )..fileOffset = fileOffset;
    case IsSetEncoding.useSentinel:
      // Generate:
      //
      //    if (isSentinel(_#field)) {
      //      return _#field = parameter;
      //    } else {
      //      throw '...';
      //    }
      return new IfStatement(
        new StaticInvocation(
          coreTypes.isSentinelMethod,
          new Arguments(<Expression>[
            createVariableRead()..fileOffset = fileOffset,
          ])..fileOffset = fileOffset,
        )..fileOffset = fileOffset,
        createReturn(
          createVariableWrite(
            new VariableGet(parameter)..fileOffset = fileOffset,
          )..fileOffset = fileOffset,
        ),
        new ExpressionStatement(exception)..fileOffset = fileOffset,
      )..fileOffset = fileOffset;
    case IsSetEncoding.useNull:
      // Generate:
      //
      //    if (_#field == null) {
      //      return _#field = parameter;
      //    } else {
      //      throw '...';
      //    }
      return new IfStatement(
        new EqualsNull(createVariableRead()..fileOffset = fileOffset)
          ..fileOffset = fileOffset,
        createReturn(
          createVariableWrite(
            new VariableGet(parameter)..fileOffset = fileOffset,
          )..fileOffset = fileOffset,
        ),
        new ExpressionStatement(exception)..fileOffset = fileOffset,
      )..fileOffset = fileOffset;
  }
}

/// Strategies for encoding whether a late field/local has been initialized.
enum IsSetEncoding {
  /// Use a boolean `isSet` field/local.
  useIsSetField,

  /// Use `null` as sentinel value to signal an uninitialized field/locals.
  useNull,

  /// Use `createSentinel`and `isSentinel` from `dart:_internal` to generate
  /// and check a sentinel value to signal an uninitialized field/local.
  useSentinel,
}

/// Strategies for encoding of late fields and locals.
enum IsSetStrategy {
  /// Always is use an `isSet` field/local to track whether the field/local has
  /// been initialized.
  forceUseIsSetField,

  /// Always use `createSentinel`and `isSentinel` from `dart:_internal` to
  /// generate and check a sentinel value to signal an uninitialized
  /// field/local.
  forceUseSentinel,

  /// For potentially nullable fields/locals use an `isSet` field/local to track
  /// whether the field/local has been initialized. Otherwise use `null` as
  /// sentinel value to signal an uninitialized field/local.
  ///
  /// This strategy can only be used with sound null safety mode. In weak mode
  /// non-nullable can be assigned `null` from legacy code and therefore `null`
  /// doesn't work as a sentinel.
  useIsSetFieldOrNull,

  /// For potentially nullable fields/locals use `createSentinel`and
  /// `isSentinel` from `dart:_internal` to generate and check a sentinel value
  /// to signal an uninitialized field/local. Otherwise use `null` as
  /// sentinel value to signal an uninitialized field/local.
  useSentinelOrNull,
}

IsSetStrategy computeIsSetStrategy(SourceLibraryBuilder libraryBuilder) {
  IsSetStrategy isSetStrategy = IsSetStrategy.useIsSetFieldOrNull;
  if (libraryBuilder.loader.target.backendTarget.supportsLateLoweringSentinel) {
    isSetStrategy = IsSetStrategy.useSentinelOrNull;
  }
  return isSetStrategy;
}

IsSetEncoding computeIsSetEncoding(DartType type, IsSetStrategy isSetStrategy) {
  switch (isSetStrategy) {
    case IsSetStrategy.forceUseIsSetField:
      return IsSetEncoding.useIsSetField;
    case IsSetStrategy.forceUseSentinel:
      return IsSetEncoding.useSentinel;
    case IsSetStrategy.useIsSetFieldOrNull:
      return type.isPotentiallyNullable
          ? IsSetEncoding.useIsSetField
          : IsSetEncoding.useNull;
    case IsSetStrategy.useSentinelOrNull:
      return type.isPotentiallyNullable
          ? IsSetEncoding.useSentinel
          : IsSetEncoding.useNull;
  }
}

/// Returns the name used for the variable that holds the value of a late
/// lowered local by the given [name].
String computeLateLocalName(String name) {
  return '${lateLocalPrefix}$name';
}

/// Returns the name used for the 'isSet' variable of a late lowered local by
/// the given [name].
String computeLateLocalIsSetName(String name) {
  return '${lateLocalPrefix}${name}${lateIsSetSuffix}';
}

/// Returns the name used for the getter function of a late lowered local by
/// the given [name].
String computeLateLocalGetterName(String name) {
  return '${lateLocalPrefix}${name}${lateLocalGetterSuffix}';
}

/// Returns the name used for the setter function of a late lowered local by
/// the given [name].
String computeLateLocalSetterName(String name) {
  return '${lateLocalPrefix}${name}${lateLocalSetterSuffix}';
}
